package nes

import (
	"fmt"
)

const (
	RAM                    = 0x0000
	RAMMirrorsEnd          = 0x1FFF
	PPURegisters           = 0x2000
	PPURegistersMirrorsEnd = 0x3FFF
)

type Bus struct {
	CpuVram          [2048]byte
	prgRom           []byte
	ppu              NesPPU
	cycles           uint16
	GameloopCallBack func(*NesPPU)
}

func NewBus(rom *Rom, fn func(*NesPPU)) *Bus {
	ppuInst := NewNesPPU(rom.ChrRom, rom.ScreenMirroring)

	return &Bus{
		CpuVram:          [2048]byte{},
		prgRom:           rom.PrgRom,
		ppu:              *ppuInst,
		cycles:           0,
		GameloopCallBack: fn,
	}
}

func (b *Bus) ReadPrgRom(addr uint16) byte {
	addr -= 0x8000
	if len(b.prgRom) == 0x4000 && addr >= 0x4000 {
		//mirror if needed
		addr = addr % 0x4000
	}
	return b.prgRom[addr]
}

// Tick the bus
func (b *Bus) Tick(cycles uint8) {
	b.cycles += uint16(cycles)
	newFrame := b.ppu.Tick(cycles * 3)

	if newFrame {
		b.GameloopCallBack(&b.ppu)
	}
}

// PollNmiStatus returns the NMI status of the PPU
func (b *Bus) PollNmiStatus() uint8 {
	return b.ppu.PollNmiInterrupt()
}

func (b *Bus) MemRead(addr uint16) byte {
	switch {
	case addr <= RAMMirrorsEnd:
		mirrorDownAddr := addr & 0b00000111_11111111
		return b.CpuVram[mirrorDownAddr]
	case addr == 0x2000 , addr == 0x2001 , addr == 0x2003 , addr == 0x2005 , addr == 0x2006 , addr == 0x4014:
		return 0
	case addr == 0x2002:
		return b.ppu.ReadStatus()
	case addr == 0x2004:
		return b.ppu.ReadOamData()
	case addr == 0x2007:
		return b.ppu.ReadData()
	case addr >= 0x4000 && addr <= 0x4015:
		return 0 // ignore APU
	case addr == 0x4016:
		return 0 // ignore joypad 1
	case addr == 0x4017:
		return 0 // ignore joypad 2
	case addr >= 0x2008 && addr <= PPURegistersMirrorsEnd:
		mirrorDownAddr := addr & 0b00100000_00000111
		return b.MemRead(mirrorDownAddr)
	case addr >= 0x8000:
		return b.ReadPrgRom(addr)
	default:
		fmt.Printf("Ignoring mem access at %v\n", addr)
		return 0
	}
}

func (b *Bus) MemWrite(addr uint16, data byte) {
	switch {
	case addr <= RAMMirrorsEnd:
		mirrorDownAddr := addr & 0b11111111111
		b.CpuVram[mirrorDownAddr] = data
	case addr == 0x2000:
		b.ppu.WriteToCtrl(data)
	case addr == 0x2001:
		b.ppu.WriteToMask(data)
	case addr == 0x2002:
		panic("attempt to write to PPU status register")
	case addr == 0x2003:
		b.ppu.WriteToOamAddr(data)
	case addr == 0x2004:
		b.ppu.WriteToOamData(data)
	case addr == 0x2005:
		b.ppu.WriteToScroll(data)
	case addr == 0x2006:
		b.ppu.WriteToPPUAddr(data)
	case addr == 0x2007:
		b.ppu.WriteToData(data)
	case addr >= 0x4000 && addr <= 0x4013 , addr == 0x4015:
		// ignore APU
	case addr == 0x4016:
		// ignore joypad 1
	case addr == 0x4017:
		// ignore joypad 2
	case addr == 0x4014:
		var buffer [256]byte
		hi := uint16(data) << 8
		for i := uint16(0); i < 256; i++ {
			buffer[i] = b.MemRead(hi + i)
		}
		b.ppu.WriteOamDMA(buffer)
		// handle this eventually
		// addCycles := 513
		// if b.cycles%2 == 1 {
		// 	addCycles = 514
		// }
		// b.Tick(addCycles) // this will cause weird effects as PPU will have 513/514 * 3 ticks
	case addr >= 0x2008 && addr <= PPURegistersMirrorsEnd:
		mirrorDownAddr := addr & 0b00100000_00000111
		b.MemWrite(mirrorDownAddr, data)
	case addr >= 0x8000:
		panic(fmt.Sprintf("Attempt to write to Cartridge ROM space: %x", addr))
	default:
		fmt.Printf("Ignoring mem write-access at %v\n", addr)
	}
}
